Completed
Push — master ( 631ad6...a78ef4 )
by thomas
01:27
created

WalletSweeper.createAddress   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 1 Features 0
Metric Value
cc 4
c 5
b 1
f 0
nc 4
dl 0
loc 44
rs 8.5806
nop 1
1
var UnspentOutputFinder = require('./unspent_output_finder');
2
var bitcoin = require('bitcoinjs-lib');
3
var bip39 = require("bip39");
4
var CryptoJS = require('crypto-js');
5
var blocktrail = require('./blocktrail');
6
var EncryptionMnemonic = require('./encryption_mnemonic');
7
var Encryption = require('./encryption');
8
var walletSDK = require('./wallet');
9
var _ = require('lodash');
10
var q = require('q');
11
var async = require('async');
12
13
/**
14
 *
15
 * @param backupData
16
 * @param bitcoinDataClient
17
 * @param options
18
 * @constructor
19
 */
20
var WalletSweeper = function(backupData, bitcoinDataClient, options) {
21
    /* jshint -W071, -W074 */
22
    var self = this;
23
    this.defaultSettings = {
24
        network: 'btc',
25
        testnet: false,
26
        regtest: false,
27
        logging: false,
28
        bitcoinCash: false,
29
        sweepBatchSize: 200
30
    };
31
    this.settings = _.merge({}, this.defaultSettings, options);
32
    this.bitcoinDataClient = bitcoinDataClient;
33
    this.utxoFinder = new UnspentOutputFinder(bitcoinDataClient, this.settings);
34
    this.sweepData = null;
35
36
    // set the bitcoinlib network
37
    if (typeof options.network === "object") {
38
        this.network = options.network;
39
    } else {
40
        this.network = this.getBitcoinNetwork(this.settings.network, this.settings.testnet, this.settings.regtest);
41
    }
42
43
    backupData.walletVersion = backupData.walletVersion || 2;   //default to version 2 wallets
44
45
    var usePassword = false;
46
47
    // validate backup data, cleanup input, and prepare seeds
48
    if (!Array.isArray(backupData.blocktrailKeys)) {
49
        throw new Error('blocktrail pub keys are required (must be type Array)');
50
    }
51
52
    switch (backupData.walletVersion) {
53
        case 1:
54
            if (typeof backupData.primaryMnemonic === "undefined" || !backupData.primaryMnemonic) {
55
                throw new Error('missing primary mnemonic for version 1 wallet');
56
            }
57
            if (typeof backupData.backupMnemonic === "undefined" || !backupData.backupMnemonic) {
58
                throw new Error('missing backup mnemonic for version 1 wallet');
59
            }
60
            if (typeof backupData.primaryPassphrase === "undefined") {
61
                throw new Error('missing primary passphrase for version 1 wallet');
62
            }
63
64
            // cleanup copy paste errors from mnemonics
65
            backupData.primaryMnemonic = backupData.primaryMnemonic.trim()
66
                .replace(new RegExp("\r\n", 'g'), " ")
67
                .replace(new RegExp("\n", 'g'), " ")
68
                .replace(/\s+/g, " ");
69
            backupData.backupMnemonic = backupData.backupMnemonic.trim()
70
                .replace(new RegExp("\r\n", 'g'), " ")
71
                .replace(new RegExp("\n", 'g'), " ")
72
                .replace(/\s+/g, " ");
73
        break;
74
75
        case 2:
76
        case 3:
77
            if (typeof backupData.encryptedPrimaryMnemonic === "undefined" || !backupData.encryptedPrimaryMnemonic) {
78
                throw new Error('missing encrypted primary seed for version 2 wallet');
79
            }
80
            if (typeof backupData.backupMnemonic === "undefined" || (!backupData.backupMnemonic && backupData.backupMnemonic !== false)) {
81
                throw new Error('missing backup seed for version 2 wallet');
82
            }
83
            //can either recover with password and password encrypted secret, or with encrypted recovery secret and a decryption key
84
            usePassword = typeof backupData.password !== "undefined" && backupData.password !== null;
85
            if (usePassword) {
86
                if (typeof backupData.passwordEncryptedSecretMnemonic === "undefined" || !backupData.passwordEncryptedSecretMnemonic) {
87
                    throw new Error('missing password encrypted secret for version 2 wallet');
88
                }
89
                if (typeof backupData.password === "undefined") {
90
                    throw new Error('missing primary passphrase for version 2 wallet');
91
                }
92
            } else {
93
                if (typeof backupData.encryptedRecoverySecretMnemonic === "undefined" || !backupData.encryptedRecoverySecretMnemonic) {
94
                    throw new Error('missing encrypted recovery secret for version 2 wallet (recovery without password)');
95
                }
96
                if (!backupData.recoverySecretDecryptionKey) {
97
                    throw new Error('missing recovery secret decryption key for version 2 wallet (recovery without password)');
98
                }
99
            }
100
101
            // cleanup copy paste errors from mnemonics
102
            backupData.encryptedPrimaryMnemonic = backupData.encryptedPrimaryMnemonic.trim()
103
                .replace(new RegExp("\r\n", 'g'), " ")
104
                .replace(new RegExp("\n", 'g'), " ")
105
                .replace(/\s+/g, " ");
106
            backupData.backupMnemonic = (backupData.backupMnemonic || "").trim()
107
                .replace(new RegExp("\r\n", 'g'), " ")
108
                .replace(new RegExp("\n", 'g'), " ")
109
                .replace(/\s+/g, " ");
110
            if (backupData.recoverySecretDecryptionKey) {
111
                backupData.recoverySecretDecryptionKey = backupData.recoverySecretDecryptionKey.trim()
112
                    .replace(new RegExp("\r\n", 'g'), " ")
113
                    .replace(new RegExp("\n", 'g'), " ")
114
                    .replace(/\s+/g, " ");
115
            }
116
            if (usePassword) {
117
                backupData.passwordEncryptedSecretMnemonic = backupData.passwordEncryptedSecretMnemonic.trim()
118
                    .replace(new RegExp("\r\n", 'g'), " ").replace(new RegExp("\n", 'g'), " ").replace(/\s+/g, " ");
119
            } else {
120
                backupData.encryptedRecoverySecretMnemonic = backupData.encryptedRecoverySecretMnemonic.trim()
121
                    .replace(new RegExp("\r\n", 'g'), " ").replace(new RegExp("\n", 'g'), " ").replace(/\s+/g, " ");
122
            }
123
124
        break;
125
126
        default:
127
            throw new Error('Wrong version [' + backupData.walletVersion + ']');
128
    }
129
130
131
    // create BIP32 HDNodes for the Blocktrail public keys
132
    this.blocktrailPublicKeys = {};
133
    _.each(backupData.blocktrailKeys, function(blocktrailKey) {
134
        self.blocktrailPublicKeys[blocktrailKey['keyIndex']] = bitcoin.HDNode.fromBase58(blocktrailKey['pubkey'], self.network);
135
    });
136
137
    // convert the primary and backup mnemonics to seeds (using BIP39)
138
    var primarySeed, backupSeed, secret;
139
    switch (backupData.walletVersion) {
140
        case 1:
141
            primarySeed = bip39.mnemonicToSeed(backupData.primaryMnemonic, backupData.primaryPassphrase);
142
            backupSeed = bip39.mnemonicToSeed(backupData.backupMnemonic, "");
143
        break;
144
145
        case 2:
146
            // convert mnemonics to hex (bip39) and then base64 for decryption
147
            backupData.encryptedPrimaryMnemonic = blocktrail.convert(bip39.mnemonicToEntropy(backupData.encryptedPrimaryMnemonic), 'hex', 'base64');
148
            if (usePassword) {
149
                backupData.passwordEncryptedSecretMnemonic = blocktrail.convert(
150
                    bip39.mnemonicToEntropy(backupData.passwordEncryptedSecretMnemonic), 'hex', 'base64');
151
            } else {
152
                backupData.encryptedRecoverySecretMnemonic = blocktrail.convert(
153
                    bip39.mnemonicToEntropy(backupData.encryptedRecoverySecretMnemonic), 'hex', 'base64');
154
            }
155
156
            // decrypt encryption secret
157
            if (usePassword) {
158
                secret = CryptoJS.AES.decrypt(backupData.passwordEncryptedSecretMnemonic, backupData.password).toString(CryptoJS.enc.Utf8);
159
            } else {
160
                secret = CryptoJS.AES.decrypt(backupData.encryptedRecoverySecretMnemonic, backupData.recoverySecretDecryptionKey).toString(CryptoJS.enc.Utf8);
161
            }
162
163
            if (!secret) {
164
                throw new Error("Could not decrypt secret with " + (usePassword ? "password" : "decryption key"));
165
            }
166
167
            // now finally decrypt the primary seed and convert to buffer (along with backup seed)
168
            primarySeed = new Buffer(CryptoJS.AES.decrypt(backupData.encryptedPrimaryMnemonic, secret).toString(CryptoJS.enc.Utf8), 'base64');
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
169
170
            if (backupData.backupMnemonic) {
171
                backupSeed = new Buffer(bip39.mnemonicToEntropy(backupData.backupMnemonic), 'hex');
172
            }
173
174
        break;
175
176
        case 3:
177
            // convert mnemonics to hex (bip39) and then base64 for decryption
178
            backupData.encryptedPrimaryMnemonic = EncryptionMnemonic.decode(backupData.encryptedPrimaryMnemonic);
179
            if (usePassword) {
180
                backupData.passwordEncryptedSecretMnemonic = EncryptionMnemonic.decode(backupData.passwordEncryptedSecretMnemonic);
181
            } else {
182
                backupData.encryptedRecoverySecretMnemonic = EncryptionMnemonic.decode(backupData.encryptedRecoverySecretMnemonic);
183
            }
184
185
            // decrypt encryption secret
186
            if (usePassword) {
187
                secret = Encryption.decrypt(backupData.passwordEncryptedSecretMnemonic, new Buffer(backupData.password));
188
            } else {
189
                secret = Encryption.decrypt(backupData.encryptedRecoverySecretMnemonic, new Buffer(backupData.recoverySecretDecryptionKey, 'hex'));
190
            }
191
192
            if (!secret) {
193
                throw new Error("Could not decrypt secret with " + (usePassword ? "password" : "decryption key"));
194
            }
195
196
            // now finally decrypt the primary seed and convert to buffer (along with backup seed)
197
            primarySeed = Encryption.decrypt(backupData.encryptedPrimaryMnemonic, secret);
198
            if (backupData.backupMnemonic) {
199
                backupSeed = new Buffer(bip39.mnemonicToEntropy(backupData.backupMnemonic), 'hex');
200
            }
201
202
        break;
203
204
        default:
205
            throw new Error('Wrong version [' + backupData.walletVersion + ']');
206
    }
207
208
    // convert the primary and backup seeds to private keys (using BIP32)
209
    this.primaryPrivateKey = bitcoin.HDNode.fromSeedBuffer(primarySeed, this.network);
210
211
    if (backupSeed) {
212
        this.backupPrivateKey = bitcoin.HDNode.fromSeedBuffer(backupSeed, this.network);
213
        this.backupPublicKey = this.backupPrivateKey.neutered();
214
    } else {
215
        this.backupPrivateKey = false;
216
        this.backupPublicKey = bitcoin.HDNode.fromBase58(backupData.backupPublicKey, this.network);
217
    }
218
219
    if (this.settings.logging) {
220
        console.log('using password method: ' + usePassword);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
221
        console.log("Primary Prv Key: " + this.primaryPrivateKey.toBase58());
222
        console.log("Primary Pub Key: " + this.primaryPrivateKey.neutered().toBase58());
223
        console.log("Backup Prv Key: " + (this.backupPrivateKey ? this.backupPrivateKey.toBase58() : null));
224
        console.log("Backup Pub Key: " + this.backupPublicKey.toBase58());
225
    }
226
};
227
228
229
/**
230
 * returns an appropriate bitcoin-js lib network
231
 *
232
 * @param network
233
 * @param testnet
234
 * @param regtest
235
 * @returns {*[]}
236
 */
237
WalletSweeper.prototype.getBitcoinNetwork =  function(network, testnet, regtest) {
238
    switch (network.toLowerCase()) {
239
        case 'btc':
240
        case 'bitcoin':
241
            if (regtest) {
242
                return bitcoin.networks.regtest;
243
            } else if (testnet) {
244
                return bitcoin.networks.testnet;
245
            } else {
246
                return bitcoin.networks.bitcoin;
247
            }
248
        break;
0 ignored issues
show
Unused Code introduced by
This break statement is unnecessary and may be removed.
Loading history...
249
        case 'tbtc':
250
        case 'bitcoin-testnet':
251
            return bitcoin.networks.testnet;
252
        default:
253
            throw new Error("Unknown network " + network);
254
    }
255
};
256
257
/**
258
 * gets the blocktrail pub key for the given path from the stored array of pub keys
259
 *
260
 * @param path
261
 * @returns {boolean}
262
 */
263
WalletSweeper.prototype.getBlocktrailPublicKey = function(path) {
264
    path = path.replace("m", "M");
265
    var keyIndex = path.split("/")[1].replace("'", "");
266
267
    if (!this.blocktrailPublicKeys[keyIndex]) {
268
        throw new Error("Wallet.getBlocktrailPublicKey keyIndex (" + keyIndex + ") is unknown to us");
269
    }
270
271
    return this.blocktrailPublicKeys[keyIndex];
272
};
273
274
/**
275
 * generate multisig address and redeem script for given path
276
 *
277
 * @param path
278
 * @returns {{address, redeem: *, witness: *}}
279
 */
280
WalletSweeper.prototype.createAddress = function(path) {
281
    //ensure a public path is used
282
    path = path.replace("m", "M");
283
    var keyIndex = path.split("/")[1].replace("'", "");
284
    var scriptType = parseInt(path.split("/")[2]);
285
286
    //derive the primary pub key directly from the primary priv key
287
    var primaryPubKey = walletSDK.deriveByPath(this.primaryPrivateKey, path, "m");
288
    //derive the backup pub key directly from the backup priv key (unharden path)
289
    var backupPubKey = walletSDK.deriveByPath(this.backupPublicKey, path.replace("'", ""), "M");
290
    //derive a pub key for this path from the blocktrail pub key
291
    var blocktrailPubKey = walletSDK.deriveByPath(this.getBlocktrailPublicKey(path), path, "M/" + keyIndex + "'");
292
293
    //sort the keys and generate a multisig redeem script and address
294
    var multisigKeys = walletSDK.sortMultiSigKeys([
295
        primaryPubKey.keyPair.getPublicKeyBuffer(),
296
        backupPubKey.keyPair.getPublicKeyBuffer(),
297
        blocktrailPubKey.keyPair.getPublicKeyBuffer()
298
    ]);
299
300
    var multisig = bitcoin.script.multisig.output.encode(2, multisigKeys);
301
    var redeemScript, witnessScript;
302
    if (this.network !== "bitcoincash" && scriptType === walletSDK.CHAIN_BTC_SEGWIT) {
303
        witnessScript = multisig;
304
        redeemScript = bitcoin.script.witnessScriptHash.output.encode(bitcoin.crypto.sha256(witnessScript));
305
    } else {
306
        witnessScript = null;
307
        redeemScript = multisig;
308
    }
309
    var scriptHash = bitcoin.crypto.hash160(redeemScript);
310
    var scriptPubKey = bitcoin.script.scriptHash.output.encode(scriptHash);
311
312
    var network = this.network;
313
    if (typeof this.network !== "undefined") {
314
        network = this.network;
315
    }
316
    var address = bitcoin.address.fromOutputScript(scriptPubKey, network, !!this.settings.bitcoinCash);
317
318
    // Insight nodes want nothing to do with 'bitcoin:' or 'bitcoincash:' prefixes
319
    address = address.replace('bitcoin:', '').replace('bitcoincash:', '');
320
321
    //@todo return as buffers
322
    return {address: address.toString(), redeem: redeemScript, witness: witnessScript};
323
};
324
325
/**
326
 * create a batch of multisig addresses
327
 *
328
 * @param start
329
 * @param count
330
 * @param keyIndex
331
 * @param chain
332
 * @returns {{}}
333
 */
334
WalletSweeper.prototype.createBatchAddresses = function(start, count, keyIndex, chain) {
335
    var self = this;
336
    var addresses = {};
337
338
    return q.all(_.range(0, count).map(function(i) {
339
        //create a path subsequent address
340
        var path =  "M/" + keyIndex + "'/" + chain + "/" + (start + i);
341
        var multisig = self.createAddress(path);
342
        addresses[multisig['address']] = {
343
            redeem: multisig['redeem'],
344
            witness: multisig['witness'],
345
            path: path
346
        };
347
    })).then(function() {
348
        return addresses;
349
    });
350
};
351
352
WalletSweeper.prototype.discoverWalletFunds = function(increment, options, cb) {
353
    var self = this;
354
    var totalBalance = 0;
355
    var totalUTXOs = 0;
356
    var totalAddressesGenerated = 0;
357
358
    if (typeof increment === "function") {
359
        cb = increment;
360
        increment = null;
361
    } else if (typeof options === "function") {
362
        cb = options;
363
        options = {};
364
    }
365
366
    if(options && !(typeof options === "object")) {
367
        console.warn("Wallet Sweeper discovery options is not an object, ignoring");
368
        options = {};
369
    }
370
371
    var addressUTXOs = {};    //addresses and their utxos, paths and redeem scripts
372
    if (typeof increment === "undefined" || !increment) {
373
        increment = this.settings.sweepBatchSize;
374
    }
375
376
    var deferred = q.defer();
377
    deferred.promise.nodeify(cb);
378
379
    var checkChain;
380
    if (this.network === "bitcoincash") {
381
        checkChain = [0, 1];
382
    } else {
383
        checkChain = [0, 1, 2];
384
    }
385
386
    async.nextTick(function() {
387
        //for each blocktrail pub key, do fund discovery on batches of addresses
388
        async.eachSeries(Object.keys(self.blocktrailPublicKeys), function(keyIndex, done) {
389
            async.eachSeries(checkChain, function(chain, done) {
390
                var i = 0;
391
                var hasTransactions = false;
392
393
                async.doWhilst(function(done) {
394
                    //do
395
                    if (self.settings.logging) {
396
                        console.log("generating addresses " + i + " -> " + (i + increment) + " using blocktrail key index: " + keyIndex + ", chain: " + chain);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
397
                    }
398
                    deferred.notify({
399
                        message: "generating addresses " + i + " -> " + (i + increment) + "",
400
                        increment: increment,
401
                        btPubKeyIndex: keyIndex,
402
                        chain: chain,
403
                        //addresses: [],
404
                        totalAddresses: totalAddressesGenerated,
405
                        addressUTXOs: addressUTXOs,
406
                        totalUTXOs: totalUTXOs,
407
                        totalBalance: totalBalance
408
                    });
409
410
                    async.nextTick(function() {
411
                        self.createBatchAddresses(i, increment, keyIndex, chain)
412
                            .then(function(batch) {
413
                                totalAddressesGenerated += Object.keys(batch).length;
414
415
                                if (self.settings.logging) {
416
                                    console.log("starting fund discovery for " + increment + " addresses...");
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
417
                                }
418
419
                                deferred.notify({
420
                                    message: "starting fund discovery for " + increment + " addresses",
421
                                    increment: increment,
422
                                    btPubKeyIndex: keyIndex,
423
                                    //addresses: addresses,
424
                                    totalAddresses: totalAddressesGenerated,
425
                                    addressUTXOs: addressUTXOs,
426
                                    totalUTXOs: totalUTXOs,
427
                                    totalBalance: totalBalance
428
                                });
429
430
                                //get the unspent outputs for this batch of addresses
431
                                return self.bitcoinDataClient.batchAddressHasTransactions(_.keys(batch)).then(function(_hasTransactions) {
432
                                    hasTransactions = _hasTransactions;
433
                                    if (self.settings.logging) {
434
                                        console.log("batch " + (hasTransactions ? "has" : "does not have") + " transactions...");
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
435
                                    }
436
437
                                    return q.when(hasTransactions)
438
                                        .then(function(hasTransactions) {
439
                                            if (!hasTransactions) {
440
                                                return;
441
                                            }
442
443
                                            //get the unspent outputs for this batch of addresses
444
                                            return self.utxoFinder.getUTXOs(_.keys(batch)).then(function(utxos) {
445
                                                if (options.excludeZeroConf) {
446
                                                    // Do not evaluate 0-confirmation UTXOs
447
                                                    // This would include double spends and other things Insight happily accepts
448
                                                    // (and keeps in mempool - even when the parent UTXO gets spent otherwise)
449
                                                    for (var address in utxos) {
450
                                                        if (utxos.hasOwnProperty(address) && Array.isArray(utxos[address])) {
451
                                                            var utxosPerAddress = utxos[address];
452
                                                            // Iterate over utxos per address
453
                                                            for (var idx = 0; idx < utxosPerAddress.length; idx++) {
454
                                                                if (utxosPerAddress[idx] &&
455
                                                                    'confirmations' in utxosPerAddress[idx]
456
                                                                    && utxosPerAddress[idx]['confirmations'] === 0) {
457
                                                                    // Delete if unconfirmed
458
                                                                    delete utxos[address][idx];
459
                                                                    utxos[address].length--;
460
                                                                    if (utxos[address].length <= 0) {
461
                                                                        delete utxos[address];
462
                                                                    }
463
                                                                }
464
                                                            }
465
                                                        }
466
                                                    }
467
                                                }
468
469
                                                // save the address utxos, along with relevant path and redeem script
470
                                                _.each(utxos, function(outputs, address) {
471
                                                    var witnessScript = null;
472
                                                    if (typeof batch[address]['witness'] !== 'undefined') {
473
                                                        witnessScript = batch[address]['witness'];
474
                                                    }
475
                                                    addressUTXOs[address] = {
476
                                                        path: batch[address]['path'],
477
                                                        redeem: batch[address]['redeem'],
478
                                                        witness: witnessScript,
479
                                                        utxos: outputs
480
                                                    };
481
482
                                                    totalUTXOs += outputs.length;
483
484
                                                    //add up the total utxo value for all addresses
485
                                                    totalBalance = _.reduce(outputs, function(carry, output) {
486
                                                        return carry + output['value'];
487
                                                    }, totalBalance);
488
489
                                                    if (self.settings.logging) {
490
                                                        console.log("found " + outputs.length + " unspent outputs for address: " + address);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
491
                                                    }
492
                                                });
493
494
                                                deferred.notify({
495
                                                    message: "discovering funds",
496
                                                    increment: increment,
497
                                                    btPubKeyIndex: keyIndex,
498
                                                    totalAddresses: totalAddressesGenerated,
499
                                                    addressUTXOs: addressUTXOs,
500
                                                    totalUTXOs: totalUTXOs,
501
                                                    totalBalance: totalBalance
502
                                                });
503
                                            });
504
                                        })
505
                                        ;
506
                                });
507
                            })
508
                            .then(
509
                                function() {
510
                                    //ready for the next batch
511
                                    i += increment;
512
                                    async.nextTick(done);
513
                                },
514
                                function(err) {
515
                                    done(err);
516
                                }
517
                            )
518
                        ;
519
                    });
520
                }, function() {
521
                    //while
522
                    return hasTransactions;
523
                }, function(err) {
524
                    //all done
525
                    if (err) {
526
                        console.log("batch complete, but with errors", err.message);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
527
528
                        deferred.notify({
529
                            message: "batch complete, but with errors: " + err.message,
530
                            error: err,
531
                            increment: increment,
532
                            btPubKeyIndex: keyIndex,
533
                            totalAddresses: totalAddressesGenerated,
534
                            addressUTXOs: addressUTXOs,
535
                            totalUTXOs: totalUTXOs,
536
                            totalBalance: totalBalance
537
                        });
538
                    }
539
                    //ready for next Blocktrail pub key
540
                    async.nextTick(done);
541
                });
542
            }, function(err) {
543
                done(err);
544
            });
545
        }, function(err) {
546
            //callback
547
            if (err) {
548
                //perhaps we should also reject the promise, and stop everything?
549
                if (self.settings.logging) {
550
                    console.log("error encountered when discovering funds", err);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
551
                }
552
            }
553
554
            if (self.settings.logging) {
555
                console.log("finished fund discovery: " + totalBalance + " Satoshi (in " + totalUTXOs + " outputs) " +
556
                    "found when searching " + totalAddressesGenerated + " addresses");
557
            }
558
559
            self.sweepData = {
560
                utxos: addressUTXOs,
561
                count: totalUTXOs,
562
                balance: totalBalance,
563
                addressesSearched: totalAddressesGenerated
564
            };
565
566
            //resolve the promise
567
            deferred.resolve(self.sweepData);
568
        });
569
    });
570
571
    return deferred.promise;
572
};
573
574
WalletSweeper.prototype.sweepWallet = function(destinationAddress, cb) {
575
    var self = this;
576
    var deferred = q.defer();
577
    deferred.promise.nodeify(cb);
578
579
    if (self.settings.logging) {
580
        console.log("starting wallet sweeping to address " + destinationAddress);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
581
    }
582
583
    q.when(true)
584
        .then(function() {
585
            if (!self.sweepData) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if !self.sweepData is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
586
                //do wallet fund discovery
587
                return self.discoverWalletFunds()
588
                    .progress(function(progress) {
589
                        deferred.notify(progress);
590
                    });
591
            }
592
        })
593
        .then(function() {
594
            return self.bitcoinDataClient.estimateFee();
595
        })
596
        .then(function(feePerKb) {
597
            // Insight reports 1000 sat/kByte, but this is too low
598
            if (self.settings.bitcoinCash && feePerKb < 5000) {
599
                feePerKb = 5000;
600
            }
601
602
            if (self.sweepData['balance'] === 0) {
603
                //no funds found
604
                deferred.reject("No funds found after searching through " + self.sweepData['addressesSearched'] + " addresses");
605
                return deferred.promise;
606
            }
607
608
            //create and sign the transaction
609
            return self.createTransaction(destinationAddress, null, feePerKb, deferred);
610
        })
611
        .then(function(r) {
612
            deferred.resolve(r);
613
        }, function(e) {
614
            deferred.reject(e);
615
        });
616
617
    return deferred.promise;
618
};
619
620
/**
621
 * creates a raw transaction from the sweep data
622
 * @param destinationAddress        the destination address for the transaction
623
 * @param fee                       a specific transaction fee to use (optional: if null, fee will be estimated)
624
 * @param feePerKb                  fee per kb (optional: if null, use default value)
625
 * @param deferred                  a deferred promise object, used for giving progress updates (optional)
626
 */
627
WalletSweeper.prototype.createTransaction = function(destinationAddress, fee, feePerKb, deferred) {
628
    var self = this;
629
    if (this.settings.logging) {
630
        console.log("Creating transaction to address destinationAddress");
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
631
    }
632
    if (deferred) {
633
        deferred.notify({
634
            message: "creating raw transaction to " + destinationAddress
635
        });
636
    }
637
638
    // create raw transaction
639
    var rawTransaction = new bitcoin.TransactionBuilder(this.network);
640
    if (this.settings.bitcoinCash) {
641
        rawTransaction.enableBitcoinCash();
642
    }
643
    var inputs = [];
644
    _.each(this.sweepData['utxos'], function(data, address) {
645
        _.each(data.utxos, function(utxo) {
646
            rawTransaction.addInput(utxo['hash'], utxo['index']);
647
            inputs.push({
648
                txid:         utxo['hash'],
649
                vout:         utxo['index'],
650
                scriptPubKey: utxo['script_hex'],
651
                value:        utxo['value'],
652
                address:      address,
653
                path:         data['path'],
654
                redeemScript: data['redeem'],
655
                witnessScript: data['witness']
656
            });
657
        });
658
    });
659
    if (!rawTransaction) {
660
        throw new Error("Failed to create raw transaction");
661
    }
662
663
    var sendAmount = self.sweepData['balance'];
664
    var outputIdx = rawTransaction.addOutput(destinationAddress, sendAmount);
665
666
    if (typeof fee === "undefined" || fee === null) {
667
        //estimate the fee and reduce it's value from the output
668
        if (deferred) {
669
            deferred.notify({
670
                message: "estimating transaction fee, based on " + blocktrail.toBTC(feePerKb) + " BTC/kb"
671
            });
672
        }
673
674
        var toHexString = function(byteArray) {
675
            return Array.prototype.map.call(byteArray, function(byte) {
676
                return ('0' + (byte & 0xFF).toString(16)).slice(-2);
677
            }).join('');
678
        };
679
680
        var calcUtxos = inputs.map(function(input) {
681
            var rs = (typeof input.redeemScript === "string" || !input.redeemScript)
682
                ? input.redeemScript : toHexString(input.redeemScript);
683
            var ws = (typeof input.witnessScript === "string" || !input.witnessScript)
684
                ? input.witnessScript : toHexString(input.witnessScript);
685
686
            return {
687
                txid: input.txid,
688
                vout: input.vout,
689
                address: input.address,
690
                scriptpubkey_hex: input.scriptPubKey,
691
                redeem_script: rs,
692
                witness_script: ws,
693
                path: input.path,
694
                value: input.value
695
            };
696
        });
697
        fee = walletSDK.estimateVsizeFee(rawTransaction.tx, calcUtxos, feePerKb);
698
    }
699
    rawTransaction.tx.outs[outputIdx].value -= fee;
700
701
    //sign and return the raw transaction
702
    if (deferred) {
703
        deferred.notify({
704
            message: "signing transaction"
705
        });
706
    }
707
    return this.signTransaction(rawTransaction, inputs);
708
};
709
710
WalletSweeper.prototype.signTransaction = function(rawTransaction, inputs) {
711
    var self = this;
712
    if (this.settings.logging) {
713
        console.log("Signing transaction");
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
714
    }
715
716
    var sigHash = bitcoin.Transaction.SIGHASH_ALL;
717
    if (this.settings.bitcoinCash) {
718
        sigHash |= bitcoin.Transaction.SIGHASH_BITCOINCASHBIP143;
719
    }
720
721
    //sign the transaction with the private key for each input
722
    _.each(inputs, function(input, index) {
723
        //create private keys for signing
724
        var primaryPrivKey =  walletSDK.deriveByPath(self.primaryPrivateKey, input['path'].replace("M", "m"), "m").keyPair;
725
        rawTransaction.sign(index, primaryPrivKey, input['redeemScript'], sigHash, input['value'], input['witnessScript']);
726
727
        if (self.backupPrivateKey) {
728
            var backupPrivKey = walletSDK.deriveByPath(self.backupPrivateKey, input['path'].replace("'", "").replace("M", "m"), "m").keyPair;
729
            rawTransaction.sign(index, backupPrivKey, input['redeemScript'], sigHash, input['value'], input['witnessScript']);
730
        }
731
    });
732
733
    if (self.backupPrivateKey) {
734
        return rawTransaction.build().toHex();
735
    } else {
736
        return rawTransaction.buildIncomplete().toHex();
737
    }
738
};
739
740
module.exports = WalletSweeper;
741